﻿namespace SharpF2.Components
{
	using System;
	using System.Collections.ObjectModel;
	using System.Windows;
	using System.Windows.Controls;
	using System.Windows.Controls.Primitives;
	using System.Windows.Input;
	using System.Windows.Media;
	using System.Windows.Media.Media3D;

	/// <summary>
	/// Interaction logic for ListViewExtended.xaml
	/// </summary>
	public partial class ListViewExtended<ItemType>
	{
		#region Public Attributes

		public static readonly DependencyProperty AfterDropCommandProperty =
			DependencyProperty.Register(
				"AfterDropCommand",
				typeof (ICommand),
				typeof (ListViewExtended<ItemType>));

		#endregion

		#region Private Attributes

		/// <summary>
		/// Can initiate drag.
		/// </summary>
		private bool canInitiateDrag;

		/// <summary>
		/// Index to select.
		/// </summary>
		private int indexToSelect;

		/// <summary>
		/// Item under drag cursor.
		/// </summary>
		private ItemType itemUnderDragCursor;

		/// <summary>
		/// Point when last mouse down.
		/// </summary>
		private Point pointMouseDown;

		#endregion

		#region Public Properties

		public ICommand AfterDropCommand
		{
			get { return (ICommand) GetValue(AfterDropCommandProperty); }
			set { SetValue(AfterDropCommandProperty, value); }
		}

		/// <summary>
		/// Gets a value indicating whether if there is currently a drag operation being managed.
		/// </summary>
		public bool IsDragInProgress { get; private set; }

		#endregion

		#region Private Properties

		/// <summary>
		/// Gets a value indicating whether CanStartDragOperation.
		/// </summary>
		private bool CanStartDragOperation
		{
			get
			{
				if (Mouse.LeftButton != MouseButtonState.Pressed)
				{
					return false;
				}

				if (!canInitiateDrag)
				{
					return false;
				}

				if (indexToSelect == -1)
				{
					return false;
				}

				if (!HasCursorLeftDragThreshold)
				{
					return false;
				}

				return true;
			}
		}

		/// <summary>
		/// Gets a value indicating whether HasCursorLeftDragThreshold.
		/// </summary>
		private bool HasCursorLeftDragThreshold
		{
			get
			{
				if (indexToSelect < 0)
				{
					return false;
				}

				var item = GetListViewItem(indexToSelect);
				var bounds = VisualTreeHelper.GetDescendantBounds(item);
				var pointInItem = TranslatePoint(pointMouseDown, item);

				// In case the cursor is at the very top or bottom of the ListViewItem
				// we want to make the vertical threshold very small so that dragging
				// over an adjacent item does not select it.
				var topOffset = Math.Abs(pointInItem.Y);
				var btmOffset = Math.Abs(bounds.Height - pointInItem.Y);
				var vertOffset = Math.Min(topOffset, btmOffset);

				var width = SystemParameters.MinimumHorizontalDragDistance * 2;
				var height = Math.Min(SystemParameters.MinimumVerticalDragDistance, vertOffset) * 2;
				var sizeThreshold = new Size(width, height);

				var rect = new Rect(pointMouseDown, sizeThreshold);
				rect.Offset(sizeThreshold.Width / -2, sizeThreshold.Height / -2);
				var pointInListView = MouseUtilities.GetMousePosition(this);
				return !rect.Contains(pointInListView);
			}
		}

		/// <summary>
		/// Gets the index of the ListViewItem underneath the
		/// drag cursor, or -1 if the cursor is not over an item.
		/// </summary>
		private int IndexUnderDragCursor
		{
			get
			{
				var index = -1;
				for (var i = 0; i < Items.Count; ++i)
				{
					var item = GetListViewItem(i);
					if (IsMouseOverTarget(item))
					{
						index = i;
						break;
					}
				}

				return index;
			}
		}

		/// <summary>
		/// Gets a value indicating whether if the mouse cursor is over a scrollbar in the ListView.
		/// </summary>
		private bool IsMouseOverScrollbar
		{
			get
			{
				var pointMouse = MouseUtilities.GetMousePosition(this);
				var res = VisualTreeHelper.HitTest(this, pointMouse);
				if (res == null)
				{
					return false;
				}

				var depObj = res.VisualHit;
				while (depObj != null)
				{
					if (depObj is ScrollBar)
					{
						return true;
					}

					// VisualTreeHelper works with objects of type Visual or Visual3D.
					// If the current object is not derived from Visual or Visual3D,
					// then use the LogicalTreeHelper to find the parent element.
					if (depObj is Visual || depObj is Visual3D)
					{
						depObj = VisualTreeHelper.GetParent(depObj);
					}
					else
					{
						depObj = LogicalTreeHelper.GetParent(depObj);
					}
				}

				return false;
			}
		}

		/// <summary>
		/// Gets or sets ItemUnderDragCursor.
		/// </summary>
		private ItemType ItemUnderDragCursor
		{
			get { return itemUnderDragCursor; }

			set
			{
				if (itemUnderDragCursor == value)
				{
					return;
				}

				// The first pass handles the previous item under the cursor.
				// The second pass handles the new one.
				for (var i = 0; i < 2; ++i)
				{
					if (i == 1)
					{
						itemUnderDragCursor = value;
					}

					if (itemUnderDragCursor != null)
					{
						var listViewItem = GetListViewItem(itemUnderDragCursor);
						if (listViewItem != null)
						{
							ListViewItemDragState.SetIsUnderDragCursor(listViewItem, i == 1);
						}
					}
				}
			}
		}

		#endregion

		#region Private Members

		/// <summary>
		/// Is mouse over
		/// </summary>
		/// <param name="target">The target object</param>
		/// <returns>
		/// True if the mouse is over the target object
		/// </returns>
		private static bool IsMouseOverTarget(Visual target)
		{
			// We need to use MouseUtilities to figure out the cursor
			// coordinates because, during a drag-drop operation, the WPF
			// mechanisms for getting the coordinates behave strangely.
			if (null != target)
			{
				var bounds = VisualTreeHelper.GetDescendantBounds(target);
				var mousePos = MouseUtilities.GetMousePosition(target);
				return bounds.Contains(mousePos);
			}

			return false;
		}

		private void DragNDropInitialize()
		{
			canInitiateDrag = false;
			indexToSelect = -1;

			AllowDrop = true;
		}

		/// <summary>
		/// Finish drag operation
		/// </summary>
		/// <param name="draggedItem">Dragged item</param>
		private void FinishDragOperation(ListViewItem draggedItem)
		{
			// Let the ListViewItem know that it is not being dragged anymore.
			ListViewItemDragState.SetIsBeingDragged(draggedItem, false);

			IsDragInProgress = false;

			if (ItemUnderDragCursor != null)
			{
				ItemUnderDragCursor = null;
			}
		}

		/// <summary>
		/// Get listView item
		/// </summary>
		/// <param name="index">The index of the item</param>
		/// <returns>
		/// The corresponding ListViewItem
		/// </returns>
		private ListViewItem GetListViewItem(int index)
		{
			if (ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
			{
				return null;
			}

			return ItemContainerGenerator.ContainerFromIndex(index) as ListViewItem;
		}

		/// <summary>
		/// Get listView item
		/// </summary>
		/// <param name="dataItem">The dataItem</param>
		/// <returns>
		/// The corresponding ListViewItem
		/// </returns>
		private ListViewItem GetListViewItem(ItemType dataItem)
		{
			if (ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
			{
				return null;
			}

			return ItemContainerGenerator.ContainerFromItem(dataItem) as ListViewItem;
		}

		/// <summary>
		/// Initialize drag operation
		/// </summary>
		/// <param name="itemToDrag">Item to drag</param>
		private void InitializeDragOperation(ListViewItem itemToDrag)
		{
			// Set some flags used during the drag operation.
			IsDragInProgress = true;
			canInitiateDrag = false;

			// Let the ListViewItem know that it is being dragged.
			ListViewItemDragState.SetIsBeingDragged(itemToDrag, true);
		}

		/// <summary>
		/// Perform drag operation
		/// </summary>
		private void PerformDragOperation()
		{
			var selectedItem = SelectedItem as ItemType;
			const DragDropEffects AllowedEffects = DragDropEffects.Move | DragDropEffects.Move | DragDropEffects.Link;
			if (selectedItem != null &&
			    DragDrop.DoDragDrop(this, selectedItem, AllowedEffects) != DragDropEffects.None)
			{
				// The item was dropped into a new location,
				// so make it the new selected item.
				SelectedItem = selectedItem;
			}
		}

		#endregion

		#region Protected Members

		/// <summary>
		/// Drag leave
		/// </summary>
		/// <param name="e">The parameter of the event</param>
		protected override void OnDragLeave(DragEventArgs e)
		{
			base.OnDragLeave(e);

			if (!IsMouseOverTarget(this))
			{
				if (ItemUnderDragCursor != null)
				{
					ItemUnderDragCursor = null;
				}
			}
		}

		/// <summary>
		/// ListView drag over
		/// </summary>
		/// <param name="e">The parameter of the event</param>
		protected override void OnDragOver(DragEventArgs e)
		{
			base.OnDragOver(e);

			e.Effects = DragDropEffects.Move;

			// Update the item which is known to be currently under the drag cursor.
			var index = IndexUnderDragCursor;
			ItemUnderDragCursor = index < 0 ? null : Items[index] as ItemType;
		}

		/// <summary>
		/// ListView drop
		/// </summary>
		/// <param name="e">The parameter of the event</param>
		/// <exception cref="Exception">ItemSource missing</exception>
		protected override void OnDrop(DragEventArgs e)
		{
			base.OnDrop(e);

			if (ItemUnderDragCursor != null)
			{
				ItemUnderDragCursor = null;
			}

			e.Effects = DragDropEffects.None;

			if (!e.Data.GetDataPresent(typeof (ItemType)))
			{
				return;
			}

			// Get the data object which was dropped.
			var data = e.Data.GetData(typeof (ItemType)) as ItemType;
			if (data == null)
			{
				return;
			}

			// Get the ObservableCollection<ItemType> which contains the dropped data object.
			var itemsSource = ItemsSource as ObservableCollection<ItemType>;
			if (itemsSource == null)
			{
				throw new Exception(
					"A ListView managed by ListViewDragManager must have its ItemsSource set to an ObservableCollection<ItemType>.");
			}

			var oldIndex = itemsSource.IndexOf(data);
			var newIndex = IndexUnderDragCursor;

			if (newIndex < 0)
			{
				// The drag started somewhere else, and our ListView is empty
				// so make the new item the first in the list.
				if (itemsSource.Count == 0)
				{
					newIndex = 0;
				}
				else if (oldIndex < 0)
				{
					// The drag started somewhere else, but our ListView has items
					// so make the new item the last in the list.
					newIndex = itemsSource.Count;
				}
				else
				{
					// The user is trying to drop an item from our ListView into
					// our ListView, but the mouse is not over an item, so don't
					// let them drop it.
					return;
				}
			}

			// Dropping an item back onto itself is not considered an actual 'drop'.
			if (oldIndex == newIndex)
			{
				return;
			}

			// Move the dragged data object from it's original index to the
			// new index (according to where the mouse cursor is).  If it was
			// not previously in the ListBox, then insert the item.
			if (oldIndex > -1)
			{
				itemsSource.Move(oldIndex, newIndex);
			}
			else
			{
				itemsSource.Insert(newIndex, data);
			}

			if (null == AfterDropCommand || !AfterDropCommand.CanExecute(null)) return;
			AfterDropCommand.Execute(null);

			// Set the Effects property so that the call to DoDragDrop will return 'Move'.
			e.Effects = DragDropEffects.Move;
		}

		/// <summary>
		/// Preview mouse left button down.
		/// </summary>
		/// <param name="e">The parameter of the event.</param>
		protected override void OnPreviewMouseLeftButtonDown(MouseButtonEventArgs e)
		{
			base.OnPreviewMouseLeftButtonDown(e);

			if (IsMouseOverScrollbar)
			{
				// 4/13/2007 - Set the flag to false when cursor is over scrollbar.
				canInitiateDrag = false;
				return;
			}

			var index = IndexUnderDragCursor;
			canInitiateDrag = index > -1;

			if (canInitiateDrag)
			{
				// Remember the location and index of the ListViewItem the user clicked on for later.
				pointMouseDown = MouseUtilities.GetMousePosition(this);
				indexToSelect = index;
			}
			else
			{
				pointMouseDown = new Point(-10000, -10000);
				indexToSelect = -1;
			}
		}

		/// <summary>
		/// Preview mouse move
		/// </summary>
		/// <param name="e">The parameter of the event</param>
		protected override void OnPreviewMouseMove(MouseEventArgs e)
		{
			base.OnPreviewMouseMove(e);

			if (!CanStartDragOperation)
			{
				return;
			}

			// Select the item the user clicked on.
			if (SelectedIndex != indexToSelect)
			{
				SelectedIndex = indexToSelect;
			}

			// If the item at the selected index is null, there's nothing
			// we can do, so just return;
			if (SelectedItem == null)
			{
				return;
			}

			var itemToDrag = GetListViewItem(SelectedIndex);
			if (itemToDrag == null)
			{
				return;
			}

			InitializeDragOperation(itemToDrag);
			PerformDragOperation();
			FinishDragOperation(itemToDrag);
		}

		#endregion
	}
}